﻿using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using System.Xml;
using NetVoxLab.Framework.Exceptions;
using NetVoxLab.Framework.Model;
using Version = NetVoxLab.Framework.Model.Version;

[assembly: CLSCompliant(true)]
namespace NetVoxLab.Framework
{
    public class Parser : IParser
    {
        private readonly ISourceReader _sourceReader;

        public Parser(ISourceReader sourceReader)
        {   
            if (sourceReader == null)
                throw new ArgumentNullException("sourceReader");

            _sourceReader = sourceReader;
        }

        public INode Parse(string source)
        {
            if (!IsBracketsConsistenced(source))
                throw new BracketsInconsistencyException();

            source = Utilities.RemoveSpecialChars(source);
            if (source.Count(c => c == '}') == 1)
                return ParseString(source);
            
            // find first part of teh string object
            int startPart1 = source.IndexOf('{');
            int endPart1;
            for (endPart1 = startPart1 + 1; endPart1 < source.Length; endPart1++)
            {
                if (source[endPart1] == '{' || source[endPart1] == '}') break;
                
            }
            string parent = source.Substring(startPart1, endPart1 - startPart1);

            // find second part of the string object
            int endPart2 = source.LastIndexOf('}');
            int startPart2;
            for (startPart2 = endPart2 - 1; startPart2 > -1; startPart2--)
            {
                if (source[startPart2] == '}')
                {
                    startPart2++;
                    break;
                }
            }
            parent += source.Substring(startPart2, endPart2 - startPart2 + 1);
            string child = source.Substring(endPart1, startPart2 - endPart1);

            var childs = GetChildsAsString(child);

            INode parentNode = ParseString(parent);

            var parentMultiNode = parentNode as IMultiNode;
            if (parentMultiNode != null)
            {
                foreach (string childNode in childs)
                {
                    INode subNode = Parse(childNode);
                    parentMultiNode.Nodes.Add(subNode);
                }
            }

            return parentNode;
        }

        internal static IEnumerable<string> GetChildsAsString(string source)
        {
            var childs = new List<string>();

            bool isProcessing = true;
            int start = source.IndexOf('{');
            int length = 1;
            int skipCount = 0;
            for (int i = start + 1; i < source.Length; i++, length++)
            {
                if (source[i] == '}')
                {   
                    if (skipCount > 0)
                    {
                        skipCount--;
                        continue;
                    }
                    string node = source.Substring(start, length + 1);
                    childs.Add(node);
                    isProcessing = false;
                }
                else if (source[i] == '{')
                {
                    if (!isProcessing)
                    {
                        length = 0;
                        start = i;
                    }
                    else
                    {
                        skipCount++;
                    }
                }
            }

            return childs;
        }

        public string ToXml(INode node)
        {
            if (node == null)
                throw new ArgumentNullException("node");

            var xml = new XmlDocument();

            XmlNode doctype = xml.CreateNode(XmlNodeType.XmlDeclaration, "", "");
            xml.AppendChild(doctype);
            XmlNode root = xml.CreateElement("root");

            // todo optimaze it!!!!
            XmlElement el = xml.CreateElement("undefined");

            var stringAttr = node as StringAttribute;
            if (stringAttr != null)
            {
                el = xml.CreateElement("StringAttribute");
                XmlAttribute attr = xml.CreateAttribute("name");
                attr.InnerText = stringAttr.Name;
                el.Attributes.Append(attr);

                attr = xml.CreateAttribute("value");
                attr.InnerText = stringAttr.Value;
                el.Attributes.Append(attr);
            }
            else if (node is IntegerAttribute)
            {
                var intAttr = node as IntegerAttribute;

                el = xml.CreateElement("IntegerAttribute");
                XmlAttribute attr = xml.CreateAttribute("name");
                attr.InnerText = intAttr.Name;
                el.Attributes.Append(attr);

                attr = xml.CreateAttribute("value");
                attr.InnerText = "" + intAttr.Value;
                el.Attributes.Append(attr);
            }
            else if (node is TypeA)
            {
                var type = node as TypeA;

                el = xml.CreateElement("TypeA");
                XmlAttribute attr = xml.CreateAttribute("name");
                attr.InnerText = type.Name;
                el.Attributes.Append(attr);

                attr = xml.CreateAttribute("capacity");
                attr.InnerText = "" + type.Capacity;
                el.Attributes.Append(attr);

                if (type.Version != null)
                {
                    attr = xml.CreateAttribute("version");
                    string version = type.Version.Integral != 0 ? "" + type.Version.Integral : "";
                    version += ".";
                    if (type.Version.Fractional != 0)
                        version += type.Version.Fractional;
                    else
                        version = version.Replace(".", "");

                    if (string.IsNullOrEmpty(version)) version = "0";
                    attr.InnerText = version;
                    el.Attributes.Append(attr);
                }
            }
            else
            {
                var type = node as TypeB;
                if (type != null)
                {
                    el = xml.CreateElement("TypeB");
                    XmlAttribute attr = xml.CreateAttribute("name");
                    attr.InnerText = type.Name;
                    el.Attributes.Append(attr);

                    attr = xml.CreateAttribute("value");
                    attr.InnerText = "" + type.Value;
                    el.Attributes.Append(attr);

                    attr = xml.CreateAttribute("url");
                    attr.InnerText = type.Url.OriginalString;
                    el.Attributes.Append(attr);
                }
                else
                {
                    var blob = node as AttachedBlob;
                    if (blob != null)
                    {
                        var encoding = new ASCIIEncoding();
                        var bytes = new byte[blob.Value.Count()];

                        for (int i = 0; i < blob.Value.Count(); i++)
                            bytes[i] = blob.Value.ElementAt(i);

                        string value = encoding.GetString(bytes);

                        el = xml.CreateElement("AttachedBlob");
                        XmlAttribute attr = xml.CreateAttribute("value");
                        attr.InnerText = value;
                        el.Attributes.Append(attr);
                    }
                }
            }

        root.AppendChild(el);
            xml.AppendChild(root);
            return xml.InnerXml;
        }

        // todo refactor later - make it use recursion
        internal static IList<string> MakeStringObjects(string source)
        {
            if (!IsBracketsConsistenced(source))
                throw new BracketsInconsistencyException();

            int count = source.Count(c => c == '{');
            var list = new List<string>();

            for (int i = 0; i < count; i++)
            {
                int startPos = source.LastIndexOf('{');
                int endpos = source.IndexOf('}');

                string subSource = source.Substring(startPos, endpos - startPos + 1);
                list.Add(subSource);

                string ancientSource = source.Substring(0, startPos);
                string end = source.Substring(endpos + 1, source.Length - endpos - 1);
                ancientSource += end;
                source = ancientSource;
            }

            return list;
        }

        public string ReadSource(string fileName)
        {
            //TextReader reader = new StreamReader(fileName);

            if (string.IsNullOrEmpty(fileName))
                throw new ArgumentException("Filename is empty!");

            string source = _sourceReader.ReadToEnd();
            _sourceReader.Close();

            if (string.IsNullOrEmpty(source))
                return source;

            if (!IsBracketsConsistenced(source))
                throw new BracketsInconsistencyException();

            return source;
        }

        internal static string ParseType(string input)
        {
            return ParseFieldValue(input, "type");
        }

        internal static Version ParseVersion(string input)
        {
            var output = new Version();

            string version = ParseFieldValue(input, "version");

            if (string.IsNullOrEmpty(version))
                return null;

            if (version.Contains("."))
            {
                // .34
                if (version[0] == '.')
                {
                    output.Integral = 0;
                    version = version.Replace(".", ""); // remove '.'
                    output.Fractional = Convert.ToInt32(version, CultureInfo.InvariantCulture);
                }
                else // 123.45
                {
                    string[] nums = version.Split(new char[] { '.' }, 2);
                    output.Integral = Convert.ToInt32(nums[0], CultureInfo.InvariantCulture);
                    output.Fractional = Convert.ToInt32(nums[1], CultureInfo.InvariantCulture);
                }
            }
            else
            {
                output.Integral = Convert.ToInt32(version, CultureInfo.InvariantCulture);
                output.Fractional = 0;
            }

            return output;
        }
        
        internal static bool IsFieldExist(string input, string field)
        {
            if (string.IsNullOrEmpty(input))
                return false;

            if (string.IsNullOrEmpty(field))
                return false;

            Match match = Regex.Match(input, string.Format(CultureInfo.InvariantCulture, "{0} *= *", field), RegexOptions.IgnoreCase | RegexOptions.Multiline);

            return match.Success;
        }

        /// <summary>
        /// Get value of particular field
        /// </summary>
        /// <param name="input">String representation of object</param>
        /// <param name="field">Particular field</param>
        /// <returns>Value of the field</returns>
        internal static string ParseFieldValue(string input, string field)
        {
            if (string.IsNullOrEmpty(input))
                throw new ArgumentNullException(input);

            if (string.IsNullOrEmpty(field))
                throw new ArgumentNullException(field);

            if (!input.ToLowerInvariant().Contains(field))
                return null;

            Match match = Regex.Match(input, string.Format(CultureInfo.InvariantCulture, "{0} *= *", field), RegexOptions.IgnoreCase | RegexOptions.Multiline);
            if (!match.Success)
                return null;

            int startPosition = match.Index;
            startPosition += match.Length;

            // todo may be add/remove '{'
            // found out it later
            var separators = new List<char> { '}', '"', '{' };

            // missing if leading '"'
            if (input[startPosition] == '"')
                startPosition++;
            else
                separators.Add(' ');
            

            var subInput = input.Substring(startPosition).Trim();
            var output = new StringBuilder();

            foreach (char t in subInput)
            {
                if (separators.Any(x => x == t))
                    break;
                
                output.Append(t);
            }

            return output.ToString();
        }

        internal static string ParseValue(string input)
        {
            return ParseFieldValue(input, "value");
        }

        internal static string ParseName(string input)
        {
            return ParseFieldValue(input, "name");
        }

        /// <summary>
        /// Used for parse TypeA object
        /// </summary>
        /// <param name="input">Text representation of TypeA object</param>
        /// <returns>Value of capacity field</returns>
        internal static int ParseCapacity(string input)
        {
            string capacity = ParseFieldValue(input, "capacity");

            if (string.IsNullOrEmpty(capacity))
                return 0;

            try
            {
                return Convert.ToInt32(capacity, CultureInfo.InvariantCulture);
            }
            catch (FormatException ex)
            {
                throw new ArgumentException("Capacity is not integer!", ex);
            }
        }

        /// <summary>
        /// Used for parse TypeB object
        /// </summary>
        /// <param name="input">Text representation of TypeB object</param>
        /// <returns>Value of url field</returns>
        internal static Uri ParseUrl(string input)
        {
            string url = ParseFieldValue(input, "url");
            if (string.IsNullOrEmpty(url))
                return null;

            try
            {
                return new Uri(url);    
            }
            catch(UriFormatException ex)
            {
                throw new ArgumentException("Url has bad format!", ex);
            }
        }

        internal static INode ParseString(string input)
        {
            // remove whitespaces
            // BAD: DO NOT USE IT!
            //source = RemoveWhitespaces(source);

            string type = ParseType(input);
            if (string.IsNullOrEmpty(type))
                throw new ArgumentException("Type not found!");
           
            INode result;
            switch (type.Trim().ToUpperInvariant())
            {
                case "TYPEA":
                    result = ParseTypeA(input);
                    break;

                case "TYPEB":
                    result = ParseTypeB(input);
                    break;

                case "INTEGERATTRIBUTE":
                    result = ParseIntegerAttribute(input);
                    break;

                case "STRINGATTRIBUTE":
                    result = ParseStringAttribute(input);
                    break;

                case "ATTACHEDBLOB":
                    result = ParseAttachedBlob(input);
                    break;

                default:
                    throw new ArgumentException("Type not identified!");
            }

            return result;
        }

        internal static AttachedBlob ParseAttachedBlob(string input)
        {
            string value = ParseValue(input);
            if (string.IsNullOrEmpty(value))
                return new AttachedBlob();
            
            var encoding = new ASCIIEncoding();
            byte[] bytes = encoding.GetBytes(value);

            return new AttachedBlob {Value = bytes};
        }
        
        internal static IntegerAttribute ParseIntegerAttribute(string input)
        {
            var intAttr = new IntegerAttribute {Name = ParseName(input)};

            string value = ParseValue(input);
            intAttr.Value = string.IsNullOrEmpty(value) ? 0 : Convert.ToInt32(value, CultureInfo.InvariantCulture);
            
            return intAttr;
        }

        internal static TypeA ParseTypeA(string input)
        {
//            var fieldNames = new string[] { "version", "capacity", "name" };
//            foreach (string field in fieldNames)
//            {
//                if (!IsFieldExist(source, field))
//                    throw new ArgumentNotFoundException("Missed argument", field);
//            }

            return new TypeA
                            {Capacity = ParseCapacity(input), Name = ParseName(input), Version = ParseVersion(input)};
        }

        internal static TypeB ParseTypeB(string input)
        {
//            var fieldNames = new string[] {"url", "value", "name"};
//            foreach(string field in fieldNames)
//            {
//                if (!IsFieldExist(source, field))
//                    throw new ArgumentNotFoundException("Missed argument", field);
//            }
            
            var typeB = new TypeB
                            {
                                Name = ParseName(input),
                                Url = ParseUrl(input)
                            };

            string value = ParseValue(input);
            if (string.IsNullOrEmpty(value))
            {
                typeB.Value = 0;
            }
            else
            {
                try
                {
                    typeB.Value = Convert.ToDouble(value, CultureInfo.InvariantCulture);
                }
                catch (FormatException ex)
                {
                    throw new ArgumentException("Value is not float.", ex);
                }
            }

            return typeB;
        }

        internal static StringAttribute ParseStringAttribute(string input)
        {
            return new StringAttribute {Value = ParseValue(input), Name = ParseName(input)};
        }

        /// <summary>
        /// Remove whitespaces from the source string
        /// </summary>
        /// <param name="str">Sting with whitespaces</param>
        /// <returns>String without whitespaces</returns>
        internal static string RemoveWhitespaces(string str)
        {
            return Regex.Replace(str, " ", string.Empty, RegexOptions.IgnoreCase | RegexOptions.Multiline);
        }

        /// <summary>
        /// Check for count of open and close brackets are the same
        /// </summary>
        /// <param name="str">Text for parse</param>
        /// <returns>True if count('{') == count('}'). False otherwise.</returns>
        internal static bool IsBracketsConsistenced(string str)
        {
            int openBracketsCount = str.Count(ch => ch == '{');
            int closeBracketsCount = str.Count(ch => ch == '}');

            return openBracketsCount == closeBracketsCount;
        }
    }
}
